/*
 * Copyright (C) 2013 Roy Wang
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.dolphinwang.imagecoverflow;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentTreeObserver;
import ohos.agp.components.ScrollHelper;
import ohos.agp.components.VelocityDetector;
import ohos.agp.render.Canvas;
import ohos.agp.render.LinearShader;
import ohos.agp.render.Paint;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.render.Shader;
import ohos.agp.utils.Color;
import ohos.agp.utils.Matrix;
import ohos.agp.utils.Point;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.app.IAbilityManager;
import ohos.data.rdb.DataObserver;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.media.image.PixelMap;
import ohos.miscservices.timeutility.Time;
import ohos.multimodalinput.event.TouchEvent;
import ohos.utils.LruBuffer;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * CoverFlow View
 *
 * @param <T> T extends CoverFlowAdapter
 */
public class CoverFlowView<T extends CoverFlowAdapter> extends Component
        implements Component.DrawTask,
        Component.EstimateSizeListener,
        Component.TouchEventListener,
        ComponentTreeObserver.ScrollChangedListener {
    /**
     * DEFAULT VISIBLE IMAGES
     */
    protected static final int DEFAULT_VISIBLE_IMAGES = 3;

    /**
     * static field
     */
    private static final String VIEW_LOG_TAG = "CoverFlowView";

    // 基础缩放值
    private static final float CARD_SCALE = 0.15f;
    private static float MOVE_POS_MULTIPLE = 3.0f;
    private static final int TOUCH_MINIMUM_MOVE = 5;
    private static final float MOVE_SPEED_MULTIPLE = 1;
    private static final float MAX_SPEED = 6.0f;
    private static final float FRICTION = 10.0f;
    private static final int LONG_CLICK_DELAY = 4 * 100;

    /**
     * the visible views left and right
     */
    protected int mVisibleImages = DEFAULT_VISIBLE_IMAGES;
    /**
     * INVALID INDEX
     */
    protected final int INVALID_INDEX = -1;

    /**
     * True if the data has changed since the last layout
     */
    protected boolean mDataSetChanged;

    /**
     * Cover Flow Gravity
     */
    protected CoverFlowGravity mGravity;

    /**
     * Cover FlowLayout Mode
     */
    protected CoverFlowLayoutMode mLayoutMode;

    private EventHandler mHandler;

    // 基础alphaֵ
    private final int ALPHA_DATUM = 76;
    private int STANDARD_ALPHA;

    /**
     * static field
     */
    private RecycleBin mRecycler;

    private T mAdapter;

    private int mVisibleImageCount;
    private int mImageCount;

    private Rect mCoverFlowPadding;

    private Matrix mImageTransformer;
    private Matrix mReflectionTransformer;

    private Paint mDrawImagePaint;

    private Map<Integer, CoverFlowCell> mImageCells;

    private int mWidth;
    private boolean mTouchMoved;
    private float mTouchStartPos;
    private float mTouchStartX;
    private float mTouchStartY;

    private float mOffset;

    private float mStartOffset;
    private long mStartTime;

    private float mStartSpeed;
    private float mDuration;
    private Runnable mAnimationRunnable;
    private VelocityDetector mVelocity;

    private int mCellHeight;
    private int mChildTranslateY;
    private int mReflectionTranslateY;

    private float reflectHeightFraction = 0.29999995f;
    private int reflectGap = 30;

    private boolean imageClickEnable = true;
    private boolean imageLongClickEnable = true;

    private StateListener mStateListener;

    private ImageLongClickListener mLongClickListener;
    private LongClickRunnable mLongClickRunnable;
    private boolean mLongClickTriggled;

    private ImageClickListener mClickListener;

    private int mTopImageIndex;

    private ScrollHelper mScroller;

    private DataObserver mDataSetObserver =
            new DataObserver() {
                @Override
                public void onChange() {
                    final int nextImageCount = mAdapter.getCount();

                    // If current index of top image will larger than total count in future,
                    // locate it to new mid.
                    if (mTopImageIndex % mImageCount > nextImageCount - 1) {
                        mOffset = nextImageCount - mVisibleImages - 1;
                    } else { // If current index top top image will less than total count in future,
                        // change mOffset to current state in first loop
                        mOffset += mVisibleImages;
                        while (mOffset < 0 || mOffset >= mImageCount) {
                            if (mOffset < 0) {
                                mOffset += mImageCount;
                            } else if (mOffset >= mImageCount) {
                                mOffset -= mImageCount;
                            } else {
                                LogUtil.info(LogUtil.DEFAULT_TAG, "mOffset:" + mOffset);
                            }
                        }
                        mOffset -= mVisibleImages;
                    }

                    mImageCount = nextImageCount;
                    resetCoverFlow();
                    invalidate();
                }
            };

    public CoverFlowView(Context context) {
        this(context, null);
    }

    public CoverFlowView(Context context, AttrSet attrs) {
        this(context, attrs, "");
    }

    public CoverFlowView(Context context, AttrSet attrs, String defStyle) {
        super(context, attrs, defStyle);
        initAttributes(attrs);
        init();
    }

    private void initAttributes(AttrSet attrs) {
        int totalVisibleChildren = AttrUtils.getInt(attrs, "visibleImage", 3);
        setVisibleImage(totalVisibleChildren);

        reflectHeightFraction = AttrUtils.getFloat(attrs, "reflectionHeight", 0.0f);
        if (reflectHeightFraction > 100) {
            reflectHeightFraction = 100;
        }
        reflectHeightFraction /= 100;

        reflectGap = AttrUtils.getDimension(attrs, "reflectionGap", 30);

        imageClickEnable = AttrUtils.getBoolean(attrs, "imageClickEnable", true);
        imageClickEnable = AttrUtils.getBoolean(attrs, "imageLongClickEnable", true);
        mGravity =
                CoverFlowGravity.values()[
                        AttrUtils.getInt(attrs, "coverflowGravity", CoverFlowGravity.CENTER_VERTICAL.ordinal())];
        mLayoutMode =
                CoverFlowLayoutMode.values()[
                        AttrUtils.getInt(attrs, "coverflowLayoutMode", CoverFlowLayoutMode.WRAP_CONTENT.ordinal())];
    }

    private void init() {
        LogUtil.info(LogUtil.DEFAULT_TAG, "init");
        setClickable(true);

        mImageTransformer = new Matrix();
        mReflectionTransformer = new Matrix();

        mDrawImagePaint = new Paint();
        mDrawImagePaint.setAntiAlias(true);
        mDrawImagePaint.setSubpixelAntiAlias(true);

        mCoverFlowPadding = new Rect();
        mScroller = new ScrollHelper();

        if (mHandler == null) {
            mHandler = new EventHandler(EventRunner.getMainEventRunner());
        }
        setEstimateSizeListener(this);
        addDrawTask(this);
        setTouchEventListener(this);
        getComponentTreeObserver().addScrolledListener(this);
        LogUtil.info(LogUtil.DEFAULT_TAG, "init end");
    }

    /**
     * if subclass override this method, should call super method.
     *
     * @param adapter extends CoverFlowAdapter
     */
    public void setAdapter(T adapter) {
        LogUtil.info(LogUtil.DEFAULT_TAG, "setAdapter");
        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        mAdapter = adapter;

        if (mAdapter != null) {
            mAdapter.registerDataSetObserver(mDataSetObserver);

            mImageCount = mAdapter.getCount();

            if (mRecycler != null) {
                mRecycler.clear();
            } else {
                mRecycler = new RecycleBin();
            }
        }

        mOffset = 0;

        resetCoverFlow();
        invalidate();
    }

    /**
     * get Adapter
     *
     * @return T
     */
    public T getAdapter() {
        return mAdapter;
    }

    /**
     * set StateListener
     *
     * @param stateListener StateListener
     */
    public void setStateListener(StateListener stateListener) {
        mStateListener = stateListener;
    }

    private void resetCoverFlow() {
        if (mImageCount < 3) {
            throw new IllegalArgumentException("total count in adapter must larger than 3!");
        }

        final int totalVisible = mVisibleImages * 2 + 1;
        if (mImageCount < totalVisible) {
            mVisibleImages = (mImageCount - 1) / 2;
        }
        mCellHeight = 0;

        STANDARD_ALPHA = (255 - ALPHA_DATUM) / mVisibleImages;

        if (mGravity == null) {
            mGravity = CoverFlowGravity.CENTER_VERTICAL;
        }

        if (mLayoutMode == null) {
            mLayoutMode = CoverFlowLayoutMode.WRAP_CONTENT;
        }

        mImageCells = new HashMap<>(mImageCount);

        mTopImageIndex = INVALID_INDEX;
        mDataSetChanged = true;
    }

    @Override
    public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) {
        if (mAdapter == null) {
            return false;
        }
        if (!mDataSetChanged) {
            return false;
        }
        mCoverFlowPadding.left = getPaddingLeft();
        mCoverFlowPadding.right = getPaddingRight();
        mCoverFlowPadding.top = getPaddingTop();
        mCoverFlowPadding.bottom = getPaddingBottom();
        int heightMode = EstimateSpec.getMode(heightMeasureSpec);
        int widthSize = EstimateSpec.getSize(widthMeasureSpec);
        int heightSize = EstimateSpec.getSize(heightMeasureSpec);

        int availableHeight = heightSize - mCoverFlowPadding.top - mCoverFlowPadding.bottom;

        int maxChildTotalHeight = 0;

        int visibleCount = (mVisibleImages << 1) + 1;
        int mid = (int) Math.floor(mOffset + 0.5);
        int leftChild = visibleCount >> 1;
        final int startPos = getIndex(mid - leftChild);

        for (int i = startPos; i < visibleCount + startPos; ++i) {
            PixelMap child = mAdapter.getImage(i);
            final int childHeight = child.getImageInfo().size.height;
            final int childTotalHeight = (int) (childHeight + childHeight * reflectHeightFraction + reflectGap);

            maxChildTotalHeight = (maxChildTotalHeight < childTotalHeight) ? childTotalHeight : maxChildTotalHeight;
        }

        heightSize = getHeightSize(heightMode, heightSize, availableHeight, maxChildTotalHeight);

        // Adjust movement in y-axis according to gravity
        if (mGravity == CoverFlowGravity.CENTER_VERTICAL) {
            mChildTranslateY = (heightSize >> 1) - (mCellHeight >> 1);
        } else if (mGravity == CoverFlowGravity.TOP) {
            mChildTranslateY = mCoverFlowPadding.top;
        } else if (mGravity == CoverFlowGravity.BOTTOM) {
            mChildTranslateY = heightSize - mCoverFlowPadding.bottom - mCellHeight;
        } else {
            LogUtil.info(LogUtil.DEFAULT_TAG, "mGravity:" + mGravity);
        }
        mReflectionTranslateY = (int) (mChildTranslateY + mCellHeight - mCellHeight * reflectHeightFraction);

        setEstimatedSize(widthSize, heightSize);
        mVisibleImageCount = visibleCount;
        mWidth = widthSize;
        return false;
    }

    private int getHeightSize(int heightMode, int heightSize, int availableHeight, int maxChildTotalHeight) {
        if (heightMode == EstimateSpec.PRECISE || heightMode == EstimateSpec.NOT_EXCEED) {
            // if height which parent provided is less than child need, scale
            // child height to parent provide
            if (availableHeight < maxChildTotalHeight) {
                mCellHeight = availableHeight;
            } else {
                // if larger than, depends on layout mode
                // if layout mode is match_parent, scale child height to parent
                // provide
                if (mLayoutMode == CoverFlowLayoutMode.MATCH_PARENT) {
                    mCellHeight = availableHeight;
                    // if layout mode is wrap_content, keep child's original
                    // height
                } else if (mLayoutMode == CoverFlowLayoutMode.WRAP_CONTENT) {
                    mCellHeight = maxChildTotalHeight;

                    // adjust parent's height
                    if (heightMode == EstimateSpec.NOT_EXCEED) {
                        heightSize = mCellHeight + mCoverFlowPadding.top + mCoverFlowPadding.bottom;
                    }
                } else {
                    LogUtil.info(LogUtil.DEFAULT_TAG, "mLayoutMode:" + mLayoutMode);
                }
            }
        } else {
            // height mode is unspecified
            if (mLayoutMode == CoverFlowLayoutMode.MATCH_PARENT) {
                mCellHeight = availableHeight;
            } else if (mLayoutMode == CoverFlowLayoutMode.WRAP_CONTENT) {
                mCellHeight = maxChildTotalHeight;
                heightSize = mCellHeight + mCoverFlowPadding.top + mCoverFlowPadding.bottom;
            } else {
                LogUtil.info(LogUtil.DEFAULT_TAG, "mLayoutMode:" + mLayoutMode);
            }
        }
        return heightSize;
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        if (mAdapter == null) {
            return;
        }
        final float offset = mOffset;
        int i;
        int mid = (int) Math.floor(offset + 0.5);

        int rightChild;
        int leftChild;
        rightChild = leftChild = mVisibleImageCount / 2;
        // draw the left children
        int startPos = mid - leftChild;
        for (i = startPos; i < mid; ++i) {
            drawChild(canvas, mid, i, i - offset);
        }

        // draw the right children
        int endPos = mid + rightChild;
        for (i = endPos; i >= mid; --i) {
            drawChild(canvas, mid, i, i - offset);
        }

        // call imageOnTop
        if ((offset - (int) offset) == 0.0f) {
            final int topImageIndex = getIndex((int) offset);

            if (mTopImageIndex != topImageIndex && mImageCells.get(mTopImageIndex) != null) {
                mImageCells.get(mTopImageIndex).isOnTop = false;
            }
            mTopImageIndex = topImageIndex;
            final CoverFlowCell cell = mImageCells.get(topImageIndex);
            cell.isOnTop = true;

            if (mStateListener != null) {
                mStateListener.imageOnTop(
                        this,
                        topImageIndex,
                        cell.showingRect.left,
                        cell.showingRect.top,
                        cell.showingRect.right,
                        cell.showingRect.bottom);
            }
        }

        if (mStateListener != null) {
            mStateListener.invalidationCompleted(this);
        }
    }

    /**
     * draw Child
     *
     * @param canvas   Canvas
     * @param mid      int
     * @param position int
     * @param offset   float
     */
    protected final void drawChild(Canvas canvas, int mid, int position, float offset) {
        int index = getIndex(position);

        final PixelMap child = mAdapter.getImage(index);
        final PixelMap reflection = obtainReflection(child).get();
        CoverFlowCell cell = mImageCells.get(index);
        if (cell == null) {
            cell = new CoverFlowCell();
            cell.index = index;
            mImageCells.put(index, cell);
        }
        cell.showingPosition = position;
        cell.width = child.getImageInfo().size.width;
        cell.height = child.getImageInfo().size.height;
        if (!child.isReleased() && canvas != null) {
            makeChildTransformer(child, mid, position, offset);
            // record cell data, only when images in "non-intermediate state"
            if ((offset - (int) offset) == 0.0f) {
                cell.mapTransform(mImageTransformer);
            }
            canvas.setMatrix(mImageTransformer);
            canvas.drawPixelMapHolder(new PixelMapHolder(child), 0, 0, mDrawImagePaint);

            if (reflection != null) {
                canvas.setMatrix(mReflectionTransformer);

                canvas.scale(
                        1f, -1f, reflection.getImageInfo().size.width / 2f, reflection.getImageInfo().size.height / 2f);
                canvas.drawPixelMapHolder(new PixelMapHolder(reflection), 0, 0, mDrawImagePaint);

                Paint paint = new Paint();
                paint.setAntiAlias(true);

                Point[] points = {new Point(0f, 0f), new Point(0f, reflection.getImageInfo().size.height)};

                float[] floats = {0.2f, 1f};
                Color[] colors = {new Color(Color.getIntColor("#FFFFFF")), new Color(Color.getIntColor("#70FFFFFF"))};

                LinearShader shader = new LinearShader(points, floats, colors, Shader.TileMode.MIRROR_TILEMODE);
                paint.setShader(shader, Paint.ShaderType.LINEAR_SHADER);

                canvas.drawRect(
                        new RectFloat(
                                0, 0, reflection.getImageInfo().size.width, reflection.getImageInfo().size.height),
                        paint);
            }
        }
    }

    /**
     * 对PixelMap进行伪3d变换
     *
     * @param child    PixelMap
     * @param mid      int
     * @param position int
     * @param offset   float
     */
    private void makeChildTransformer(PixelMap child, int mid, int position, float offset) {
        mImageTransformer.reset();
        mReflectionTransformer.reset();
        float spreadScale = 0;
        // this scale make sure that each image will be smaller than the // previous one
        if (position != mid) {
            spreadScale = 1 - Math.abs(offset) * 0.25f;
        } else {
            spreadScale = 1 - Math.abs(offset) * CARD_SCALE;
        }
        // 延x轴移动的距离应该根据center图片决定
        float translateX = 0;
        int childWidth = BitmapUtils.getPixMapWidth(child);
        int childHeight = BitmapUtils.getPixMapHeight(child);
        final int imageHeight = (int) (mCellHeight - mCellHeight * reflectHeightFraction - reflectGap);
        final int cellHeight = (int) (childHeight + childHeight * reflectHeightFraction + reflectGap);
        final float imageScale = (float) imageHeight / childHeight;
        final float totallyScale = imageScale * spreadScale;
        final int showingWidth = (int) (childWidth * totallyScale);
        final int centerShowingWidth = (int) (childWidth * imageScale);
        int leftSpace = ((mWidth / 2) - mCoverFlowPadding.left) - (centerShowingWidth / 2);
        int rightSpace = (((mWidth / 2) - mCoverFlowPadding.right) - (centerShowingWidth / 2));
        if (offset <= 0) {
            translateX = ((float) leftSpace / mVisibleImages) * (mVisibleImages + offset) + mCoverFlowPadding.left;
        } else {
            translateX =
                    mWidth
                            - ((float) rightSpace / mVisibleImages) * (mVisibleImages - offset)
                            - showingWidth
                            - mCoverFlowPadding.right;
        }
        float alpha = (float) 254 - Math.abs(offset) * STANDARD_ALPHA;
        if (alpha < 0) {
            alpha = 0;
        } else if (alpha > 254) {
            alpha = 254;
        } else {
            LogUtil.info(LogUtil.DEFAULT_TAG, "makeChildTransformer alpha:" + alpha);
        }
        float adjustedChildTranslateY = 0;
        if (totallyScale != 1) {
            adjustedChildTranslateY = (mCellHeight - cellHeight) / 2f;
        }
        mDrawImagePaint.setAlpha(alpha / 254);
        mImageTransformer.preTranslate(0, -(cellHeight / 2f));
        // matrix中的postxxx为顺序执行，相反prexxx为倒叙执行
        mImageTransformer.postScale(totallyScale, totallyScale);
        mImageTransformer.postTranslate(translateX, mChildTranslateY + adjustedChildTranslateY);
        mImageTransformer.postTranslate(0, (cellHeight / 2f));
        mReflectionTransformer.preTranslate(0, -(cellHeight / 2f));
        mReflectionTransformer.postScale(totallyScale, totallyScale);
        mReflectionTransformer.postTranslate(translateX, mReflectionTranslateY * spreadScale + adjustedChildTranslateY);
        mReflectionTransformer.postTranslate(0, (cellHeight / 2f));
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent event) {
        int action = event.getAction();
        switch (action) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                if (mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    invalidate();
                }
                touchBegan(event);
                return true;
            case TouchEvent.POINT_MOVE:
                touchMoved(event);
                return true;
            case TouchEvent.PRIMARY_POINT_UP:
                touchEnded(event);
                return true;
        }

        return false;
    }

    private void startLongClick(float x, float y) {
        if (imageLongClickable()) {
            final int touchedImageIndex = findTouchedImage(x, y, mTopImageIndex);
            if (touchedImageIndex != INVALID_INDEX) {
                mLongClickRunnable.setPosition(touchedImageIndex);
                postDelayed(mLongClickRunnable, LONG_CLICK_DELAY);
            }
        }
    }

    private void resetLongClick() {
        if (mLongClickRunnable != null) {
            removeCallbacks(mLongClickRunnable);
        }
        mLongClickTriggled = false;
    }

    private void postDelayed(Runnable runnable, long timeDelay) {
        if (mHandler == null) {
            return;
        }
        mHandler.postTask(runnable, timeDelay);
    }

    private void removeCallbacks(Runnable runnable) {
        if (mHandler == null) {
            return;
        }
        mHandler.removeTask(runnable);
    }

    private void post(Runnable runnable) {
        if (mHandler == null) {
            return;
        }
        mHandler.postTask(runnable);
    }

    private int findTouchedImage(float x, float y, int mid) {
        final int leftStart = mid - (mVisibleImageCount / 2);
        for (int i = mid; i >= leftStart; i--) {
            if (i >= mImageCount) {
                i -= mImageCount;
            } else if (i < 0) {
                i += mImageCount;
            } else {
                LogUtil.info(LogUtil.DEFAULT_TAG, "findTouchedImage mid:" + mid);
            }

            CoverFlowCell cell = mImageCells.get(i);
            if (cell.inTouchArea(x, y)) {
                return i;
            }
        }

        final int rightStart = mid + 1;
        for (int i = rightStart; i <= mid + (mVisibleImageCount / 2); i++) {
            if (i >= mImageCount) {
                i -= mImageCount;
            } else if (i < 0) {
                i += mImageCount;
            } else {
                LogUtil.info(LogUtil.DEFAULT_TAG, "findTouchedImage i:" + i + ";mImageCount:" + mImageCount);
            }

            CoverFlowCell cell = mImageCells.get(i);
            if (cell.inTouchArea(x, y)) {
                return i;
            }
        }

        return INVALID_INDEX;
    }

    private boolean imageLongClickable() {
        return (mLongClickListener != null && imageLongClickEnable);
    }

    private boolean imageClickable() {
        return (mClickListener != null && imageClickEnable);
    }

    private void touchBegan(TouchEvent event) {
        endAnimation();

        float x = event.getPointerPosition(0).getX();
        mTouchStartX = x;
        mTouchStartY = event.getPointerPosition(0).getY();
        mStartTime = Time.getRealActiveTime();
        mStartOffset = mOffset;

        mTouchMoved = false;

        mTouchStartPos = (x / mWidth) * MOVE_POS_MULTIPLE - 5;
        mTouchStartPos /= 2;

        mVelocity = VelocityDetector.obtainInstance();
        mVelocity.addEvent(event);

        // reset first
        resetLongClick();
        // than post an runnable to start lone clock event monitor
        startLongClick(mTouchStartX, mTouchStartY);
    }

    private void touchMoved(TouchEvent event) {
        float pos = (event.getPointerPosition(0).getX() / mWidth) * MOVE_POS_MULTIPLE - 5;
        pos /= 2;

        if (!mTouchMoved) {
            float dx = Math.abs(event.getPointerPosition(0).getX() - mTouchStartX);
            float dy = Math.abs(event.getPointerPosition(0).getY() - mTouchStartY);

            if (dx < TOUCH_MINIMUM_MOVE && dy < TOUCH_MINIMUM_MOVE) {
                return;
            }
            mTouchMoved = true;

            resetLongClick();
        }

        mOffset = mStartOffset + mTouchStartPos - pos;

        invalidate();
        mVelocity.addEvent(event);
    }

    private void touchEnded(TouchEvent event) {
        float pos = (event.getPointerPosition(0).getX() / mWidth) * MOVE_POS_MULTIPLE - 5;
        pos /= 2;

        if (mTouchMoved || (mOffset - Math.floor(mOffset)) != 0) {
            mStartOffset += mTouchStartPos - pos;
            mOffset = mStartOffset;

            mVelocity.addEvent(event);

            mVelocity.calculateCurrentVelocity(1000);
            double speed = mVelocity.getHorizontalVelocity();

            speed = (speed / mWidth) * MOVE_SPEED_MULTIPLE;
            if (speed > MAX_SPEED) {
                speed = MAX_SPEED;
            } else if (speed < -MAX_SPEED) {
                speed = -MAX_SPEED;
            } else {
                LogUtil.info(LogUtil.DEFAULT_TAG, "speed:" + speed);
            }

            startAnimation(-speed);
        } else {
            final float x = event.getPointerPosition(0).getX();
            final float y = event.getPointerPosition(0).getY();
            if (imageClickable() && !mLongClickTriggled) {
                final int touchedImageIndex = findTouchedImage(x, y, mTopImageIndex);
                if (touchedImageIndex != INVALID_INDEX) {
                    mClickListener.onClick(this, touchedImageIndex);
                }
            }
        }

        mVelocity.clear();

        resetLongClick();
    }

    private void startAnimation(double speed) {
        if (mAnimationRunnable != null) {
            return;
        }
        double delta = speed * speed / (FRICTION * 2);
        if (speed < 0) {
            delta = -delta;
        }
        double nearest = mStartOffset + delta;
        nearest = Math.floor(nearest + 0.5);

        mStartSpeed = (float) Math.sqrt(Math.abs(nearest - mStartOffset) * FRICTION * 2);
        if (nearest < mStartOffset) {
            mStartSpeed = -mStartSpeed;
        }
        mDuration = Math.abs(mStartSpeed / FRICTION);
        mStartTime = Time.getRealActiveTime();

        mAnimationRunnable =
                new Runnable() {
                    @Override
                    public void run() {
                        driveAnimation();
                    }
                };
        post(mAnimationRunnable);
    }

    private void driveAnimation() {
        float elapsed = (Time.getRealActiveTime() - mStartTime) / 1000.0f;
        if (elapsed >= mDuration) {
            endAnimation();
        } else {
            updateAnimationAtElapsed(elapsed);
            post(mAnimationRunnable);
        }
    }

    private void endAnimation() {
        if (mAnimationRunnable != null) {
            mOffset = (float) Math.floor(mOffset + 0.5);

            invalidate();

            removeCallbacks(mAnimationRunnable);
            mAnimationRunnable = null;
        }
    }

    private void updateAnimationAtElapsed(float el) {
        float elapsed = el;
        if (elapsed > mDuration) {
            elapsed = mDuration;
        }
        float delta = Math.abs(mStartSpeed) * elapsed - FRICTION * elapsed * elapsed / 2;
        if (mStartSpeed < 0) {
            delta = -delta;
        }
        mOffset = mStartOffset + delta;
        invalidate();
    }

    /**
     * Convert showing position to index in adapter
     *
     * @param position position to draw
     * @return int
     */
    private int getIndex(int position) {
        int showingPosition = position;
        if (mAdapter == null) {
            return INVALID_INDEX;
        }

        int max = mAdapter.getCount();

        showingPosition += mVisibleImages;
        while (showingPosition < 0 || showingPosition >= max) {
            if (showingPosition < 0) {
                showingPosition += max;
            } else if (showingPosition >= max) {
                showingPosition -= max;
            } else {
                LogUtil.info(LogUtil.DEFAULT_TAG, "getIndex showingPosition:" + showingPosition + ";max:" + max);
            }
        }

        return showingPosition;
    }

    private Optional<PixelMap> obtainReflection(PixelMap src) {
        if (reflectHeightFraction <= 0) {
            return Optional.empty();
        }

        PixelMap reflection = mRecycler.getCachedReflectiuon(src);
        if (reflection == null || reflection.isReleased()) {
            mRecycler.removeReflectionCache(src);
            reflection = BitmapUtils.createReflectedBitmap(src, reflectHeightFraction).get();
            mRecycler.buildReflectionCache(src, reflection);
        }
        return Optional.of(reflection);
    }

    /**
     * set Visible Image
     *
     * @param count int
     */
    public void setVisibleImage(int count) {
        if (count % 2 == 0) {
            throw new IllegalArgumentException("visible image must be an odd number");
        }

        if (count < 3) {
            throw new IllegalArgumentException("visible image must larger than 3");
        }

        mVisibleImages = count / 2;
        STANDARD_ALPHA = (255 - ALPHA_DATUM) / mVisibleImages;
    }

    /**
     * set CoverFlow Gravity
     *
     * @param gravity CoverFlowGravity
     */
    public void setCoverFlowGravity(CoverFlowGravity gravity) {
        mGravity = gravity;
    }

    /**
     * set CoverFlowLayout Mode
     *
     * @param mode CoverFlowLayoutMode
     */
    public void setCoverFlowLayoutMode(CoverFlowLayoutMode mode) {
        mLayoutMode = mode;
    }

    /**
     * set Reflection Height
     *
     * @param fraction int
     */
    public void setReflectionHeight(int fraction) {
        int fc = fraction;
        if (fc < 0) {
            fc = 0;
        } else if (fc > 100) {
            fc = 100;
        } else {
            LogUtil.info(LogUtil.DEFAULT_TAG, "setReflectionHeight fraction:" + fc);
        }

        reflectHeightFraction = fc / 100f;
    }

    /**
     * setReflectionGap
     *
     * @param gap int
     */
    public void setReflectionGap(int gap) {
        reflectGap = gap < 0 ? 0 : gap;
    }

    /**
     * disable ImageClick
     */
    public void disableImageClick() {
        imageClickEnable = false;
    }

    /**
     * enable ImageClick
     */
    public void enableImageClick() {
        imageClickEnable = true;
    }

    /**
     * disable ImageLongClick
     */
    public void disableImageLongClick() {
        imageLongClickEnable = false;
    }

    /**
     * enable ImageLongClick
     */
    public void enableImageLongClick() {
        imageLongClickEnable = true;
    }

    /**
     * set Selection position
     *
     * @param selectionPosition int
     */
    public void setSelection(int selectionPosition) {
        int position = selectionPosition;
        final int max = mAdapter.getCount();
        if (position < 0 || position >= max) {
            throw new IllegalArgumentException(
                    "Position want to select can not less than 0 or larger than max of adapter provide!");
        }

        if (mTopImageIndex != position) {
            if (mScroller.isFinished()) {
                mScroller.abortAnimation();
            }

            final int fromX = (int) (mOffset * 100);

            final CoverFlowCell destCell = mImageCells.get(position);
            final CoverFlowCell midCell = mImageCells.get(mTopImageIndex);

            if (destCell.showingRect.right > midCell.showingRect.right && position < mTopImageIndex) {
                position += mImageCount;
            } else if (destCell.showingRect.left < midCell.showingRect.left && position > mTopImageIndex) {
                position -= mImageCount;
            } else {
                LogUtil.info(LogUtil.DEFAULT_TAG, "setSelection else");
            }

            final int indexGap = position - mTopImageIndex;
            final int disX = indexGap * 100;

            mScroller.startScroll(fromX, 0, disX, 0);
            updateScroll();
        }
    }

    private synchronized void updateScroll() {
        new Thread(
                () -> {
                    boolean isStop = false;
                    do {
                        if (mScroller.isFinished()) {
                            isStop = true;
                        }
                        getContext()
                                .getUITaskDispatcher()
                                .asyncDispatch(
                                        () -> {
                                            if (mScroller.updateScroll()) {
                                                final int currX = mScroller.getCurrValue(ScrollHelper.AXIS_X);
                                                mOffset = (float) currX / 100f;
                                                invalidate();
                                            }
                                        });
                    } while (!isStop);
                })
                .start();
    }

    @Override
    public void onScrolled() {
        LogUtil.info(LogUtil.DEFAULT_TAG, "onScrolled:" + mScroller.isFinished());
        if (mScroller.isFinished()) {
            final int currX = mScroller.getScrollDistanceX();
            mOffset = (float) currX / 100;

            invalidate();
        }
    }

    /**
     * set Image LongClick Listener
     *
     * @param listener ImageLongClickListener
     */
    public void setImageLongClickListener(ImageLongClickListener listener) {
        mLongClickListener = listener;

        if (listener == null) {
            mLongClickRunnable = null;
        } else {
            if (mLongClickRunnable == null) {
                mLongClickRunnable = new LongClickRunnable();
            }
        }
    }

    /**
     * set ImageClick Listener
     *
     * @param listener ImageClickListener
     */
    public void setImageClickListener(ImageClickListener listener) {
        mClickListener = listener;
    }

    /**
     * get Top Image Index
     *
     * @return int
     */
    public int getTopImageIndex() {
        if (mTopImageIndex == INVALID_INDEX) {
            return -1;
        }

        return mTopImageIndex;
    }

    private class LongClickRunnable implements Runnable {
        private int position;

        public void setPosition(int position) {
            this.position = position;
        }

        @Override
        public void run() {
            if (mLongClickListener != null) {
                mLongClickListener.onLongClick(com.dolphinwang.imagecoverflow.CoverFlowView.this, position);
                mLongClickTriggled = true;
            }
        }
    }

    class RecycleBin {
        LruBuffer<Integer, PixelMap> bitmapCache =
                new LruBuffer<Integer, PixelMap>(getCacheSize(getContext())) {
                    @Override
                    protected void afterRemoval(boolean isEvict, Integer key, PixelMap value, PixelMap newValue) {
                        super.afterRemoval(isEvict, key, value, newValue);
                        if (isEvict && value != null && !value.isReleased()) {
                            value.release();
                        }
                    }
                };

        /**
         * get Cached Reflectiuon
         *
         * @param origin PixelMap
         * @return PixelMap
         */
        public PixelMap getCachedReflectiuon(PixelMap origin) {
            return bitmapCache.get(origin.hashCode());
        }

        /**
         * build Reflection Cache
         *
         * @param origin   PixelMap
         * @param pixelMap PixelMap
         */
        public void buildReflectionCache(PixelMap origin, PixelMap pixelMap) {
            bitmapCache.put(origin.hashCode(), pixelMap);
            Runtime.getRuntime().gc();
        }

        /**
         * remove Reflection Cache
         *
         * @param origin PixelMap
         * @return PixelMap
         */
        public Optional<PixelMap> removeReflectionCache(PixelMap origin) {
            if (origin == null) {
                return Optional.empty();
            }
            if (!bitmapCache.contains(origin.hashCode())) {
                return Optional.empty();
            }
            return bitmapCache.remove(origin.hashCode());
        }

        /**
         * clear bitmapCache
         */
        public void clear() {
            bitmapCache.clear();
        }

        private int getCacheSize(Context context) {
            IAbilityManager am = context.getAbilityManager();
            final int memClass = am.getAppMemory();
            // Target ~5% of the available heap.
            int cacheSize = 1024 * 1024 * memClass / 21;

            LogUtil.error(VIEW_LOG_TAG, "cacheSize == " + cacheSize);
            return cacheSize;
        }
    }

    /**
     * Image Long Click Listener
     */
    public interface ImageLongClickListener {
        void onLongClick(com.dolphinwang.imagecoverflow.CoverFlowView coverFlowView, int position);
    }

    /**
     * Image Click Listener
     */
    public interface ImageClickListener {
        void onClick(com.dolphinwang.imagecoverflow.CoverFlowView coverFlowView, int position);
    }

    /**
     * State Listener
     */
    public interface StateListener {
        void imageOnTop(
                com.dolphinwang.imagecoverflow.CoverFlowView coverFlowView,
                int position,
                float left,
                float top,
                float right,
                float bottom);

        void invalidationCompleted(com.dolphinwang.imagecoverflow.CoverFlowView coverFlowView);
    }

    /**
     * enum CoverFlow Gravity
     */
    public enum CoverFlowGravity {
        TOP,
        BOTTOM,
        CENTER_VERTICAL
    }

    /**
     * enum CoverFlow Layout Mode
     */
    public enum CoverFlowLayoutMode {
        MATCH_PARENT,
        WRAP_CONTENT
    }
}
